/**
 * Ticket auto-close: tickets that sit with the merchant (R_Request.IsMerchantAction)
 * and show no activity for N days (default 10) are closed, get a visible
 * "closed due to inactivity" entry in the chat history and the merchant is mailed.
 *
 * Runs with the iDempiere SERVICE token (no h3 event — timer plugin
 * server/plugins/ticket-auto-close.ts) and is also callable from the staff route
 * POST /api/requests/requests/auto-close ({ dryRun, days }).
 *
 * "No activity" = max(R_Request.Updated, newest r_requestupdate.Created) is older
 * than the cut-off. Both come back as Berlin wall-clock with a fake Z
 * (.claude/rules/idempiere/timestamps-timezone.md) → idempiereLocalToInstantMs.
 *
 * Order per ticket: close (PUT) → chat note (POST r_requestupdate, public 'C')
 * → merchant e-mail. A failed note or mail never undoes the close; both are
 * reported in the run summary. Once IsComplete is true the ticket never matches
 * again, so a re-run is idempotent. Reopened tickets (IsComplete false again)
 * only re-qualify when staff hand them back to the merchant.
 */
import nodemailer from 'nodemailer'
import { string } from 'alga-js'
import { idempiereLocalToInstantMs, berlinParts, pad2 } from './berlinTime'
import { APP_BASE_URL } from './emailVerification'

export const DEFAULT_INACTIVE_DAYS = 10
const MAX_CANDIDATES = 200
const MAIL_FROM = 'no-reply@logyou.de'
/** Marker in the chat note — lets a reader (and a future check) recognise auto-closed tickets. */
export const AUTO_CLOSE_MARKER = '[Auto-Close]'

export interface AutoCloseItem {
  id: number
  docNo: string
  orgId: number | null
  orgName: string
  lastActivity: string | null
  inactiveDays: number
  action: 'closed' | 'would-close' | 'skipped' | 'error'
  email: string
  emailSent: boolean
  noteAdded: boolean
  reason?: string
}

export interface AutoCloseSummary {
  runId: string
  trigger: string
  dryRun: boolean
  days: number
  cutoff: string
  scanned: number
  closed: number
  skipped: number
  errors: number
  durationMs: number
  items: AutoCloseItem[]
  error?: string
}

type Fetch = (path: string, method?: 'GET' | 'POST' | 'PUT', body?: any) => Promise<any>

const serviceFetch = (): Fetch => {
  const config: any = useRuntimeConfig()
  const base = String(config.api?.url || '').replace(/\/$/, '')
  const token = config.api?.idempieretoken
  if (!base || !token) throw new Error('iDempiere service token / url not configured (api.url, api.idempieretoken)')
  return (path, method = 'GET', body = null) => $fetch<any>(`${base}/${path}`, {
    method,
    retry: 0,
    timeout: 90_000,
    headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + token },
    ...(body ? { body } : {})
  } as any)
}

const enc = (s: string) => string.urlEncode(s)

/** Berlin wall-clock literal for a `$filter` on Created/Updated (compared against DB local time). */
const berlinFilterLiteral = (instantMs: number) => {
  const p = berlinParts(instantMs)
  return `${p.y}-${pad2(p.m)}-${pad2(p.d)} ${pad2(p.h)}:${pad2(p.mi)}:${pad2(p.s)}`
}

const escapeHtml = (s: string) => String(s || '')
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;').replace(/'/g, '&#39;')

// Same subject derivation as action-notification.post.ts: R_Request.Name, else
// the first two lines of the initial message (Summary).
const deriveSubject = (name: string, summary: string) => {
  if (name) return name
  const lines = String(summary || '').split(/\r?\n/).map(l => l.trim()).filter(Boolean)
  if (!lines.length) return ''
  const text = lines.slice(0, 2).join('\n')
  if (text.length > 160) return text.slice(0, 157).trimEnd() + '…'
  return lines.length > 2 ? text + ' …' : text
}

export const buildAutoCloseNote = (days: number) =>
  `${AUTO_CLOSE_MARKER} Dieses Ticket wurde automatisch geschlossen, da seit ${days} Tagen keine Aktivität erfolgte ` +
  `(es wartete auf eine Rückmeldung des Händlers). Falls das Anliegen noch offen ist, kann das Ticket jederzeit ` +
  `wieder geöffnet oder einfach mit einer neuen Nachricht beantwortet werden.\n` +
  `This ticket was closed automatically after ${days} days without activity while waiting for the merchant's reply. ` +
  `If the matter is still open, simply reopen the ticket or reply with a new message.`

const buildMerchantHtml = (o: { docNo: string, subject: string, days: number, link: string, isGerman: boolean }) => {
  const { docNo, subject, days, link, isGerman } = o
  const accent = '#d97706'
  const title = isGerman ? 'Ihr Ticket wurde wegen Inaktivität geschlossen' : 'Your ticket was closed due to inactivity'
  const intro = isGerman
    ? `Ihr Ticket wartete seit ${days} Tagen auf Ihre Rückmeldung und wurde daher automatisch geschlossen.`
    : `Your ticket had been waiting ${days} days for your reply and was therefore closed automatically.`
  const hint = isGerman
    ? 'Ist Ihr Anliegen noch offen? Dann öffnen Sie das Ticket einfach wieder oder antworten Sie mit einer neuen Nachricht – wir kümmern uns darum.'
    : 'Is your request still open? Simply reopen the ticket or reply with a new message and we will take care of it.'
  const cta = isGerman ? 'Ticket öffnen' : 'Open ticket'
  const subjectLabel = isGerman ? 'Thema' : 'Subject'
  const footer = isGerman
    ? 'Diese E-Mail wurde automatisch versendet. Bitte antworten Sie nicht direkt auf diese E-Mail.'
    : 'This email was sent automatically. Please do not reply directly to this email.'
  return `<!DOCTYPE html><html><head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.6;color:#333;background:#f4f4f4;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f4;"><tr><td style="padding:20px 0;">
<table width="600" cellpadding="0" cellspacing="0" style="margin:0 auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 4px rgba(0,0,0,0.1);">
<tr><td style="background:${accent};padding:30px 40px;text-align:center;">
<h1 style="margin:0;color:#fff;font-size:22px;">${title}</h1>
</td></tr>
<tr><td style="padding:32px 40px;">
<p style="margin:0 0 16px 0;">${intro}</p>
<table cellpadding="0" cellspacing="0" style="width:100%;border-collapse:collapse;margin:16px 0;border:1px solid #e5e7eb;border-radius:6px;">
<tr style="background:#f9fafb;"><td style="padding:10px 14px;border-bottom:1px solid #e5e7eb;font-weight:600;width:120px;">Ticket</td><td style="padding:10px 14px;border-bottom:1px solid #e5e7eb;">#${escapeHtml(docNo)}</td></tr>
<tr><td style="padding:10px 14px;font-weight:600;">${subjectLabel}</td><td style="padding:10px 14px;">${escapeHtml(subject).replace(/\n/g, '<br>') || '-'}</td></tr>
</table>
<div style="margin:16px 0;padding:14px 16px;background:#fffbeb;border-left:3px solid ${accent};border-radius:4px;color:#78350f;">${hint}</div>
<p style="text-align:center;margin:24px 0;"><a href="${link}" style="display:inline-block;padding:12px 28px;background:${accent};color:#fff;text-decoration:none;border-radius:6px;font-weight:600;">${cta}</a></p>
</td></tr>
<tr><td style="background:#f8f9fa;padding:16px 40px;text-align:center;border-top:1px solid #eee;">
<p style="margin:0;font-size:12px;color:#888;">${footer}</p>
</td></tr>
</table></td></tr></table></body></html>`
}

/** Merchant contact of a ticket's org: ad_org.C_BPartner_ID → c_bpartner EMail / return_email / AD_Language. */
const loadMerchantContact = async (f: Fetch, orgId: number | null, cache: Map<number, { to: string[], isGerman: boolean }>) => {
  if (!orgId) return { to: [] as string[], isGerman: true }
  const hit = cache.get(orgId)
  if (hit) return hit
  let contact = { to: [] as string[], isGerman: true }
  try {
    const org: any = await f(`models/ad_org/${orgId}?$select=C_BPartner_ID`)
    const bpId = org?.C_BPartner_ID?.id
    if (bpId) {
      const bp: any = await f(`models/c_bpartner/${bpId}?$select=EMail,AD_Language,return_email`)
      const to: string[] = []
      const email = String(bp?.EMail || '').trim()
      const ret = String(bp?.return_email || '').trim()
      if (email) to.push(email)
      if (ret && ret !== email) to.push(ret)
      contact = { to, isGerman: String(bp?.AD_Language?.id || 'de_DE').startsWith('de') }
    }
  } catch (e: any) {
    console.warn(`[ticket-auto-close] merchant contact for org ${orgId} failed: ${e?.message || e}`)
  }
  cache.set(orgId, contact)
  return contact
}

let running = false

export const runTicketAutoClose = async (opts: { dryRun?: boolean, days?: number, trigger?: string } = {}): Promise<AutoCloseSummary> => {
  const started = Date.now()
  const runId = Math.random().toString(36).slice(2, 8)
  const days = Math.max(1, Math.floor(Number(opts.days) || DEFAULT_INACTIVE_DAYS))
  const dryRun = opts.dryRun === true
  const cutoffMs = started - days * 86_400_000
  const summary: AutoCloseSummary = {
    runId, trigger: opts.trigger || 'manual', dryRun, days, cutoff: new Date(cutoffMs).toISOString(),
    scanned: 0, closed: 0, skipped: 0, errors: 0, durationMs: 0, items: []
  }
  if (running) {
    summary.error = 'A run is already in progress'
    summary.durationMs = Date.now() - started
    return summary
  }
  running = true
  try {
    const f = serviceFetch()
    const filter = `IsActive eq true and IsComplete eq false and IsMerchantAction eq true and Updated le '${berlinFilterLiteral(cutoffMs)}'`
    const res: any = await f(`models/r_request?$filter=${enc(filter)}&$select=DocumentNo,Name,Summary,Updated,AD_Org_ID&$orderby=${enc('Updated asc')}&$top=${MAX_CANDIDATES}`)
    const candidates: any[] = res?.records || []
    summary.scanned = candidates.length
    const contactCache = new Map<number, { to: string[], isGerman: boolean }>()
    const note = buildAutoCloseNote(days)
    const transporter = dryRun ? null : nodemailer.createTransport({ host: 'localhost', port: 25, secure: false, tls: { rejectUnauthorized: false } })

    for (const t of candidates) {
      const id = Number(t.id)
      const item: AutoCloseItem = {
        id, docNo: String(t.DocumentNo || id), orgId: t.AD_Org_ID?.id ?? null, orgName: t.AD_Org_ID?.identifier || '',
        lastActivity: null, inactiveDays: 0, action: 'skipped', email: '', emailSent: false, noteAdded: false
      }
      summary.items.push(item)
      try {
        // Newest chat entry — a merchant reply does not necessarily bump R_Request.Updated.
        let lastMs = idempiereLocalToInstantMs(t.Updated) ?? 0
        const upd: any = await f(`models/r_requestupdate?$filter=${enc('R_Request_ID eq ' + id)}&$select=Created&$orderby=${enc('Created desc')}&$top=1`)
        const newest = upd?.records?.[0]?.Created
        const newestMs = idempiereLocalToInstantMs(newest)
        if (newestMs && newestMs > lastMs) lastMs = newestMs
        item.lastActivity = lastMs ? new Date(lastMs).toISOString() : null
        item.inactiveDays = lastMs ? Math.floor((started - lastMs) / 86_400_000) : 0
        if (lastMs > cutoffMs) {
          item.reason = 'recent chat activity'
          summary.skipped++
          continue
        }

        const contact = await loadMerchantContact(f, item.orgId, contactCache)
        item.email = contact.to.join(', ')

        if (dryRun) {
          item.action = 'would-close'
          summary.closed++
          continue
        }

        // 1. close + hand back (same flags as the merchant's own "Ticket gelöst")
        await f(`models/r_request/${id}`, 'PUT', { isComplete: true, isMerchantAction: false, tableName: 'R_Request' })
        item.action = 'closed'
        summary.closed++

        // 2. visible chat-history entry (public, the ticket's own org — see request-update/index.post.ts)
        try {
          await f('models/r_requestupdate', 'POST', {
            ...(item.orgId ? { AD_Org_ID: { id: item.orgId, tableName: 'AD_Org' } } : {}),
            R_Request_ID: { id, tableName: 'R_Request' },
            ConfidentialTypeEntry: { id: 'C' },
            result: note,
            tableName: 'R_RequestUpdate'
          })
          item.noteAdded = true
        } catch (e: any) {
          item.reason = 'note failed: ' + (e?.data?.detail || e?.message || e)
          console.error(`[ticket-auto-close] note for ticket ${id} failed:`, item.reason)
        }

        // 3. merchant e-mail
        if (!contact.to.length) {
          item.reason = (item.reason ? item.reason + '; ' : '') + 'no merchant e-mail'
          continue
        }
        try {
          const subject = deriveSubject(String(t.Name || '').trim(), String(t.Summary || ''))
          await transporter!.sendMail({
            from: MAIL_FROM,
            to: contact.to.join(', '),
            subject: contact.isGerman
              ? `[Ticket #${item.docNo}] Ihr Ticket wurde wegen Inaktivität geschlossen`
              : `[Ticket #${item.docNo}] Your ticket was closed due to inactivity`,
            html: buildMerchantHtml({ docNo: item.docNo, subject, days, link: `${APP_BASE_URL}/requests/my-tickets/${id}`, isGerman: contact.isGerman })
          })
          item.emailSent = true
        } catch (e: any) {
          item.reason = (item.reason ? item.reason + '; ' : '') + 'mail failed: ' + (e?.message || e)
          console.error(`[ticket-auto-close] mail for ticket ${id} failed:`, e?.message || e)
        }
      } catch (e: any) {
        item.action = 'error'
        item.reason = e?.data?.detail || e?.data?.message || e?.message || String(e)
        summary.errors++
        console.error(`[ticket-auto-close] ticket ${id} failed:`, item.reason)
      }
    }
  } catch (e: any) {
    summary.error = e?.data?.detail || e?.message || String(e)
    console.error('[ticket-auto-close] run failed:', summary.error)
  } finally {
    running = false
    summary.durationMs = Date.now() - started
  }
  return summary
}
